// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include <zencore/zencore.h>

#include <string_view>

ZEN_THIRD_PARTY_INCLUDES_START
#include <spdlog/formatter.h>
ZEN_THIRD_PARTY_INCLUDES_END

namespace zen::logging {

using namespace std::literals;

class json_formatter final : public spdlog::formatter
{
public:
	json_formatter(std::string_view LogId) : m_LogId(LogId) {}

	virtual std::unique_ptr<formatter> clone() const override { return std::make_unique<json_formatter>(m_LogId); }

	virtual void format(const spdlog::details::log_msg& msg, spdlog::memory_buf_t& dest) override
	{
		using std::chrono::duration_cast;
		using std::chrono::milliseconds;
		using std::chrono::seconds;

		auto secs = std::chrono::duration_cast<seconds>(msg.time.time_since_epoch());
		if (secs != m_LastLogSecs)
		{
			m_CachedTm	  = spdlog::details::os::localtime(spdlog::log_clock::to_time_t(msg.time));
			m_LastLogSecs = secs;
		}

		const auto& tm_time = m_CachedTm;

		// cache the date/time part for the next second.

		if (m_CacheTimestamp != secs || m_CachedDatetime.size() == 0)
		{
			m_CachedDatetime.clear();

			spdlog::details::fmt_helper::append_int(tm_time.tm_year + 1900, m_CachedDatetime);
			m_CachedDatetime.push_back('-');

			spdlog::details::fmt_helper::pad2(tm_time.tm_mon + 1, m_CachedDatetime);
			m_CachedDatetime.push_back('-');

			spdlog::details::fmt_helper::pad2(tm_time.tm_mday, m_CachedDatetime);
			m_CachedDatetime.push_back(' ');

			spdlog::details::fmt_helper::pad2(tm_time.tm_hour, m_CachedDatetime);
			m_CachedDatetime.push_back(':');

			spdlog::details::fmt_helper::pad2(tm_time.tm_min, m_CachedDatetime);
			m_CachedDatetime.push_back(':');

			spdlog::details::fmt_helper::pad2(tm_time.tm_sec, m_CachedDatetime);

			m_CachedDatetime.push_back('.');

			m_CacheTimestamp = secs;
		}
		dest.append("{"sv);
		dest.append("\"time\": \""sv);
		dest.append(m_CachedDatetime.begin(), m_CachedDatetime.end());
		auto millis = spdlog::details::fmt_helper::time_fraction<milliseconds>(msg.time);
		spdlog::details::fmt_helper::pad3(static_cast<uint32_t>(millis.count()), dest);
		dest.append("\", "sv);

		dest.append("\"status\": \""sv);
		dest.append(spdlog::level::to_string_view(msg.level));
		dest.append("\", "sv);

		dest.append("\"source\": \""sv);
		dest.append("zenserver"sv);
		dest.append("\", "sv);

		dest.append("\"service\": \""sv);
		dest.append("zencache"sv);
		dest.append("\", "sv);

		if (!m_LogId.empty())
		{
			dest.append("\"id\": \""sv);
			dest.append(m_LogId);
			dest.append("\", "sv);
		}

		if (msg.logger_name.size() > 0)
		{
			dest.append("\"logger.name\": \""sv);
			dest.append(msg.logger_name);
			dest.append("\", "sv);
		}

		if (msg.thread_id != 0)
		{
			dest.append("\"logger.thread_name\": \""sv);
			spdlog::details::fmt_helper::pad_uint(msg.thread_id, 0, dest);
			dest.append("\", "sv);
		}

		if (!msg.source.empty())
		{
			dest.append("\"file\": \""sv);
			WriteEscapedString(
				dest,
				spdlog::details::short_filename_formatter<spdlog::details::null_scoped_padder>::basename(msg.source.filename));
			dest.append("\","sv);

			dest.append("\"line\": \""sv);
			dest.append(fmt::format("{}", msg.source.line));
			dest.append("\","sv);

			dest.append("\"logger.method_name\": \""sv);
			WriteEscapedString(dest, msg.source.funcname);
			dest.append("\", "sv);
		}

		dest.append("\"message\": \""sv);
		WriteEscapedString(dest, msg.payload);
		dest.append("\""sv);

		dest.append("}\n"sv);
	}

private:
	static inline const std::unordered_map<char, std::string_view> SpecialCharacterMap{{'\b', "\\b"sv},
																					   {'\f', "\\f"sv},
																					   {'\n', "\\n"sv},
																					   {'\r', "\\r"sv},
																					   {'\t', "\\t"sv},
																					   {'"', "\\\""sv},
																					   {'\\', "\\\\"sv}};

	static void WriteEscapedString(spdlog::memory_buf_t& dest, const spdlog::string_view_t& payload)
	{
		const char* RangeStart = payload.begin();
		for (const char* It = RangeStart; It != payload.end(); ++It)
		{
			if (auto SpecialIt = SpecialCharacterMap.find(*It); SpecialIt != SpecialCharacterMap.end())
			{
				if (RangeStart != It)
				{
					dest.append(RangeStart, It);
				}
				dest.append(SpecialIt->second);
				RangeStart = It + 1;
			}
		}
		if (RangeStart != payload.end())
		{
			dest.append(RangeStart, payload.end());
		}
	};

	std::tm				 m_CachedTm{0, 0, 0, 0, 0, 0, 0, 0, 0};
	std::chrono::seconds m_LastLogSecs{0};
	std::chrono::seconds m_CacheTimestamp{0};
	spdlog::memory_buf_t m_CachedDatetime;
	std::string			 m_LogId;
};

}  // namespace zen::logging
